import os
import unittest
import tempfile
from datetime import datetime
from io import StringIO
from pathlib import Path

from robot.errors import DataError
from robot.result import ExecutionResult, ExecutionResultBuilder, Result, TestSuite
from robot.utils.asserts import assert_equal, assert_false, assert_true, assert_raises


CURDIR = Path(__file__).resolve().parent
GOLDEN_XML = (CURDIR / 'golden.xml').read_text(encoding='UTF-8')
GOLDEN_XML_TWICE = (CURDIR / 'goldenTwice.xml').read_text(encoding='UTF-8')
SUITE_TEARDOWN_FAILED = (CURDIR / 'suite_teardown_failed.xml').read_text(encoding='UTF-8')


class TestBuildingSuiteExecutionResult(unittest.TestCase):

    def setUp(self):
        self.result = ExecutionResult(StringIO(GOLDEN_XML))
        self.suite = self.result.suite
        self.test = self.suite.tests[0]

    def test_result_has_generation_time(self):
        assert_equal(self.result.generation_time, datetime(2023, 9, 8, 12, 1, 47, 906104))
        result = ExecutionResult("<robot><suite/></robot>")
        assert_equal(result.generation_time, None)
        result = ExecutionResult("<robot generated='20111024 13:41:20.873'><suite/></robot>")
        assert_equal(result.generation_time, datetime(2011, 10, 24, 13, 41, 20, 873000))

    def test_generation_time_can_be_set_as_string(self):
        dt = datetime.now()
        result = Result(generation_time=dt.isoformat())
        assert_equal(result.generation_time, dt)

    def test_suite_is_built(self):
        assert_equal(self.suite.source, Path('normal.html'))
        assert_equal(self.suite.name, 'Normal')
        assert_equal(self.suite.doc, 'Normal test cases')
        assert_equal(self.suite.metadata, {'Something': 'My Value', 'Nön-ÄSCÏÏ': '🤖'})
        assert_equal(self.suite.status, 'PASS')
        assert_equal(self.suite.starttime, '20111024 13:41:20.873')
        assert_equal(self.suite.endtime, '20111024 13:41:20.952')
        assert_equal(self.suite.statistics.passed, 1)
        assert_equal(self.suite.statistics.failed, 0)

    def test_testcase_is_built(self):
        assert_equal(self.test.name, 'First One')
        assert_equal(self.test.doc, 'Test case documentation')
        assert_equal(self.test.timeout, None)
        assert_equal(list(self.test.tags), ['t1'])
        assert_equal(len(self.test.body), 6)
        assert_equal(self.test.status, 'PASS')
        assert_equal(self.test.starttime, '20111024 13:41:20.925')
        assert_equal(self.test.endtime, '20111024 13:41:20.934')

    def test_keyword_is_built(self):
        keyword = self.test.body[0]
        assert_equal(keyword.full_name, 'BuiltIn.Log')
        assert_equal(keyword.doc, 'Logs the given message with the given level.')
        assert_equal(keyword.args, ('Test 1',))
        assert_equal(keyword.assign, ())
        assert_equal(keyword.status, 'PASS')
        assert_equal(keyword.starttime, '20111024 13:41:20.926')
        assert_equal(keyword.endtime, '20111024 13:41:20.928')
        assert_equal(keyword.timeout, None)
        assert_equal(len(keyword.body), 1)
        assert_equal(keyword.body[0].type, keyword.body[0].MESSAGE)

    def test_user_keyword_is_built(self):
        user_keyword = self.test.body[1]
        assert_equal(user_keyword.name, 'logs on trace')
        assert_equal(user_keyword.doc, '')
        assert_equal(user_keyword.args, ())
        assert_equal(user_keyword.assign, ('${not really in source}',))
        assert_equal(user_keyword.status, 'PASS')
        assert_equal(user_keyword.starttime, '20111024 13:41:20.930')
        assert_equal(user_keyword.endtime, '20111024 13:41:20.933')
        assert_equal(user_keyword.timeout, None)
        assert_equal(len(user_keyword.messages), 0)
        assert_equal(len(user_keyword.body), 1)

    def test_message_is_built(self):
        message = self.test.body[0].messages[0]
        assert_equal(message.message, 'Test 1')
        assert_equal(message.level, 'INFO')
        assert_equal(message.timestamp, datetime(2011, 10, 24, 13, 41, 20, 927000))

    def test_for_is_built(self):
        for_ = self.test.body[2]
        assert_equal(for_.flavor, 'IN')
        assert_equal(for_.assign, ('${x}',))
        assert_equal(for_.values, ('not in source',))
        assert_equal(len(for_.body), 1)
        assert_equal(for_.body[0].assign, {'${x}': 'not in source'})
        assert_equal(len(for_.body[0].body), 1)
        kw = for_.body[0].body[0]
        assert_equal(kw.full_name, 'BuiltIn.Log')
        assert_equal(kw.args, ('${x}',))
        assert_equal(len(kw.body), 1)
        assert_equal(kw.body[0].message, 'not in source')

    def test_if_is_built(self):
        root = self.test.body[3]
        if_, else_ = root.body
        assert_equal(if_.condition, "'IF' == 'WRONG'")
        assert_equal(if_.status, if_.NOT_RUN)
        assert_equal(len(if_.body), 1)
        kw = if_.body[0]
        assert_equal(kw.full_name, 'BuiltIn.Fail')
        assert_equal(kw.status, kw.NOT_RUN)
        assert_equal(else_.condition, None)
        assert_equal(else_.status, else_.PASS)
        assert_equal(len(else_.body), 1)
        kw = else_.body[0]
        assert_equal(kw.full_name, 'BuiltIn.No Operation')
        assert_equal(kw.status, kw.PASS)

    def test_suite_setup_is_built(self):
        assert_equal(len(self.suite.setup.body), 0)
        assert_equal(len(self.suite.setup.messages), 0)

    def test_errors_are_built(self):
        assert_equal(len(self.result.errors.messages), 1)
        assert_equal(self.result.errors.messages[0].message,
                     "Error in file 'normal.html' in table 'Settings': "
                     "Resource file 'nope' does not exist.")

    def test_omit_keywords(self):
        result = ExecutionResult(StringIO(GOLDEN_XML), include_keywords=False)
        assert_equal(len(result.suite.tests[0].body), 0)

    def test_omit_keywords_during_xml_parsing(self):
        class NonVisitingSuite(TestSuite):
            def visit(self, visitor):
                pass
        result = Result(suite=NonVisitingSuite())
        builder = ExecutionResultBuilder(StringIO(GOLDEN_XML), include_keywords=False)
        builder.build(result)
        assert_equal(len(result.suite.tests[0].body), 0)

    def test_rpa_with_xml(self):
        rpa_false = GOLDEN_XML
        self._validate_rpa(ExecutionResult(rpa_false), False)
        self._validate_rpa(ExecutionResult(rpa_false, rpa=True), True)
        rpa_true = rpa_false.replace('rpa="false"', 'rpa="true"')
        self._validate_rpa(ExecutionResult(rpa_true), True)
        self._validate_rpa(ExecutionResult(rpa_true, rpa=False), False)

    def test_rpa_with_json(self):
        rpa_false = ExecutionResult(GOLDEN_XML).to_json()
        self._validate_rpa(ExecutionResult(rpa_false), False)
        self._validate_rpa(ExecutionResult(rpa_false, rpa=True), True)
        rpa_true = rpa_false.replace('"rpa":false', '"rpa":true')
        self._validate_rpa(ExecutionResult(rpa_true), True)
        self._validate_rpa(ExecutionResult(rpa_true, rpa=False), False)

    def _validate_rpa(self, result, expected):
        assert_equal(result.rpa, expected)
        if isinstance(result, Result):
            children = [result.suite]
        else:
            children = result.suites
        for child in children:
            self._validate_rpa(child, expected)


class TestCombiningSuites(unittest.TestCase):

    def setUp(self):
        self.result = ExecutionResult(StringIO(GOLDEN_XML), StringIO(GOLDEN_XML))

    def test_name(self):
        assert_equal(self.result.suite.name, 'Normal & Normal')


class TestMergingSuites(unittest.TestCase):

    def setUp(self):
        result = ExecutionResult(StringIO(GOLDEN_XML), StringIO(GOLDEN_XML),
                                 StringIO(GOLDEN_XML), merge=True)
        self.suite = result.suite
        self.test = self.suite.tests[0]

    def test_name(self):
        assert_equal(self.suite.name, 'Normal')
        assert_equal(self.test.name, 'First One')

    def test_message(self):
        message = self.test.message
        assert_true(message.startswith('*HTML* <span class="merge">Test has been re-executed and results merged.</span><hr>'))
        assert_true('<span class="new-status">New status:</span> <span class="pass">PASS</span>' in message)
        assert_equal(message.count('<span class="new-status">'), 1)
        assert_true('<span class="new-message">New message:</span>' not in message)
        assert_true('<span class="old-status">Old status:</span> <span class="pass">PASS</span>' in message)
        assert_equal(message.count('<span class="old-status">'), 2)
        assert_true('<span class="old-message">Old message:</span>' not in message)


class TestElements(unittest.TestCase):

    def test_nested_suites(self):
        xml = """
        <robot>
        <suite name="foo">
          <suite name="bar">
            <suite name="quux">
            </suite>
          </suite>
        </suite>
        </robot>
        """
        suite = ExecutionResult(StringIO(xml)).suite
        assert_equal(suite.name, 'foo')
        assert_equal(suite.suites[0].name, 'bar')
        assert_equal(suite.longname, 'foo')
        assert_equal(suite.suites[0].longname, 'foo.bar')
        assert_equal(suite.suites[0].suites[0].name, 'quux')
        assert_equal(suite.suites[0].suites[0].longname, 'foo.bar.quux')

    def test_test_message(self):
        xml = """
        <robot>
        <suite name="foo">
          <test name="test">
            <status status="FAIL">Failure message</status>
          </test>
        </suite>
        </robot>
        """
        test = ExecutionResult(StringIO(xml)).suite.tests[0]
        assert_equal(test.message, 'Failure message')
        assert_equal(test.status, 'FAIL')
        assert_equal(test.longname, 'foo.test')

    def test_suite_message(self):
        xml = """
        <robot>
        <suite name="foo">
          <status status="FAIL">Setup failed</status>
        </suite>
        </robot>
        """
        suite = ExecutionResult(StringIO(xml)).suite
        assert_equal(suite.message, 'Setup failed')

    def test_unknown_elements_cause_an_error(self):
        assert_raises(DataError, ExecutionResult, StringIO('<some_tag/>'))


class TestSuiteTeardownFailed(unittest.TestCase):

    def test_passed_test(self):
        tc = ExecutionResult(StringIO(SUITE_TEARDOWN_FAILED)).suite.tests[0]
        assert_equal(tc.status, 'FAIL')
        assert_equal(tc.message, 'Parent suite teardown failed:\nXXX')

    def test_failed_test(self):
        tc = ExecutionResult(StringIO(SUITE_TEARDOWN_FAILED)).suite.tests[1]
        assert_equal(tc.status, 'FAIL')
        assert_equal(tc.message, 'Message\n\n'
                                  'Also parent suite teardown failed:\nXXX')

    def test_already_processed(self):
        inp = SUITE_TEARDOWN_FAILED.replace('generator="Robot', 'generator="Rebot')
        passed, failed, teardowns = ExecutionResult(StringIO(inp)).suite.tests
        assert_equal(passed.status, 'PASS')
        assert_equal(passed.message, '')
        assert_equal(failed.status, 'FAIL')
        assert_equal(failed.message, 'Message')
        assert_equal(teardowns.status, 'PASS')
        assert_equal(teardowns.message, '')

    def test_excluding_keywords(self):
        suite = ExecutionResult(StringIO(SUITE_TEARDOWN_FAILED),
                                include_keywords=False).suite
        passed, failed, teardowns = suite.tests
        assert_equal(passed.status, 'FAIL')
        assert_equal(passed.message, 'Parent suite teardown failed:\nXXX')
        assert_equal(failed.status, 'FAIL')
        assert_equal(failed.message, 'Message\n\n'
                                     'Also parent suite teardown failed:\nXXX')
        assert_equal(teardowns.status, 'FAIL')
        assert_equal(teardowns.message, 'Parent suite teardown failed:\nXXX')
        for item in suite.setup, suite.teardown:
            assert_false(item)
        for item in passed, failed, teardowns:
            assert_equal(list(item.body), [])

    def test_excluding_keywords_and_already_processed(self):
        inp = SUITE_TEARDOWN_FAILED.replace('generator="Robot', 'generator="Rebot')
        suite = ExecutionResult(StringIO(inp), include_keywords=False).suite
        passed, failed, teardowns = suite.tests
        assert_equal(passed.status, 'PASS')
        assert_equal(passed.message, '')
        assert_equal(failed.status, 'FAIL')
        assert_equal(failed.message, 'Message')
        assert_equal(teardowns.status, 'PASS')
        assert_equal(teardowns.message, '')
        for item in suite.setup, suite.teardown:
            assert_false(item)
        for item in passed, failed, teardowns:
            assert_equal(list(item.body), [])


class TestBuildingFromXmlStringAndHandlingMissingInformation(unittest.TestCase):

    def setUp(self):
        self.result = """
        <robot>
            <suite name="foo">
                <test name="some name">
                    <status status="PASS"></status>
                </test>
            <status status="PASS"></status>
            </suite>
        </robot>
        """
        self.string_result = ExecutionResult(self.result)
        self.byte_string_result = ExecutionResult(self.result.encode('UTF-8'))

    def test_suite_from_string(self):
        suite = self.string_result.suite
        self._test_suite(suite)

    def test_test_from_string(self):
        test = self.string_result.suite.tests[0]
        self._test_test(test)

    def test_suite_from_byte_string(self):
        suite = self.byte_string_result.suite
        self._test_suite(suite)

    def test_test_from_byte_string(self):
        test = self.byte_string_result.suite.tests[0]
        self._test_test(test)

    @staticmethod
    def _test_suite(suite):
        assert_equal(suite.id, 's1')
        assert_equal(suite.name, 'foo')
        assert_equal(suite.doc, '')
        assert_equal(suite.source, None)
        assert_equal(suite.metadata, {})
        assert_equal(suite.starttime, None)
        assert_equal(suite.endtime, None)
        assert_equal(suite.elapsedtime, 0)

    @staticmethod
    def _test_test(test):
        assert_equal(test.id, 's1-t1')
        assert_equal(test.name, 'some name')
        assert_equal(test.doc, '')
        assert_equal(test.timeout, None)
        assert_equal(list(test.tags), [])
        assert_equal(list(test.body), [])
        assert_equal(test.starttime, None)
        assert_equal(test.endtime, None)
        assert_equal(test.elapsedtime, 0)


class TestUsingPathlibPath(unittest.TestCase):

    def setUp(self):
        self.result = ExecutionResult(Path(__file__).parent / 'golden.xml')

    def test_suite_is_built(self, suite=None):
        suite = suite or self.result.suite
        assert_equal(suite.source, Path('normal.html'))
        assert_equal(suite.name, 'Normal')
        assert_equal(suite.doc, 'Normal test cases')
        assert_equal(suite.metadata, {'Something': 'My Value', 'Nön-ÄSCÏÏ': '🤖'})
        assert_equal(suite.status, 'PASS')
        assert_equal(suite.starttime, '20111024 13:41:20.873')
        assert_equal(suite.endtime, '20111024 13:41:20.952')
        assert_equal(suite.statistics.passed, 1)
        assert_equal(suite.statistics.failed, 0)

    def test_test_is_built(self, suite=None):
        test = (suite or self.result.suite).tests[0]
        assert_equal(test.name, 'First One')
        assert_equal(test.doc, 'Test case documentation')
        assert_equal(test.timeout, None)
        assert_equal(list(test.tags), ['t1'])
        assert_equal(len(test.body), 6)
        assert_equal(test.status, 'PASS')
        assert_equal(test.starttime, '20111024 13:41:20.925')
        assert_equal(test.endtime, '20111024 13:41:20.934')

    def test_save(self):
        temp = os.getenv('TEMPDIR', tempfile.gettempdir())
        path = Path(temp) / 'pathlib.xml'
        self.result.save(path)
        try:
            result = ExecutionResult(path)
        finally:
            path.unlink()
        self.test_suite_is_built(result.suite)
        self.test_test_is_built(result.suite)


if __name__ == '__main__':
    unittest.main()
